/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package simhya.simengine.stochastic.jep.old;


import simhya.simengine.stochastic.StochasticSimulator;
import simhya.GlobalOptions;
import simhya.simengine.utils.*;
import simhya.dataprocessing.DataCollector;
import simhya.model.stochastic.StochasticModel;
import simhya.simengine.ProgressMonitor;
import simhya.simengine.SimulationException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.ArrayList;
import simhya.model.transition.TType;
import cern.jet.random.engine.RandomEngine;

import simhya.utils.*;


/**
 *
 * @author Luca
 */
public class SSASimulator implements StochasticSimulator {
    private DataCollector collector;
    private StochasticModel model;
    private boolean initialized;

    private double initialTime;
    private double currentTime;
    private double finalTime;

    private double lastSimulationTime;
    private long lastSimulationStochasticSteps;
    private long lastSimulationInstantaneousSteps;
    private long lastSimulationTimedSteps;
    private long lastSimulationDelayedSteps;
    

    private EventQueue eventQueue;
    private RateManager rateManager;
    private InstantaneousEventsManager eventManager;

    private boolean forceRateManager;
    private RMType  forcedState;

    private RandomEngine rand;

    private NextEvent next;


    public SSASimulator(StochasticModel model, DataCollector collector) {
        this.model = model;
        this.collector = collector;
        init();
    }
    /**
     * finalizes construction of the object
     */
    private void init() {
        this.initialTime = 0;
        this.forceRateManager = false;
        this.forcedState = RMType.LIST;
        this.initialized = false;
        this.eventQueue = null;
        this.eventManager = null;
        this.rateManager = null;
        rand = RandomGenerator.getNewRandomGenerator();
        this.lastSimulationTime = 0;
        this.lastSimulationStochasticSteps = 0;
        this.lastSimulationInstantaneousSteps = 0;
        this.lastSimulationTimedSteps = 0;
        this.lastSimulationDelayedSteps = 0;
        next = null;
    }


    /**
     * Initializes the simulator, setting it up so that it is ready to run
     */
    public void initialize() {
        if (this.initialized)
            return;
        if (model == null || collector == null)
            throw new SimulationException("Model or data collector not defined");
        int[] idList;
        //initialize rate manager, for stochastic transitions
        idList = model.getListOfStochasticTransitionID();
        double[] rates = new double[model.getNumberOfStochasticTransitions()];
        for (int i=0;i<idList.length;i++) {
            int id = idList[i];
            if (model.evaluateGuard(id))
                rates[i] = model.computeRate(id);
            else
                rates[i] = 0.0;
        }
        if (this.forceRateManager) {
            if (forcedState == RMType.LIST)
                rateManager = new RateList(model.getNumberOfTransitions(),idList,rates);
            else
                rateManager = new RateTree(model.getNumberOfTransitions(),idList,rates);
        } else {
            if (model.getNumberOfTransitions() < GlobalOptions.transitionThresholdForRateTree)
                rateManager = new RateList(model.getNumberOfTransitions(),idList,rates);
            else
                rateManager = new RateTree(model.getNumberOfTransitions(),idList,rates);
        }

        //initializing  timed transitions
        idList = model.getListOfTimedTransitionsID();
        eventQueue = new EventQueue(model.getNumberOfTransitions());
        for (Integer id : idList)
            if (model.evaluateGuard(id))
                eventQueue.addEvent(id, model.computeNextFiringTime(id, initialTime));
            else
                eventQueue.addEvent(id,Double.POSITIVE_INFINITY);
        
        //initializing instantaneous transitions
        this.eventManager = new InstantaneousEventsManager(model.getNumberOfInstantaneousTransitions());
        this.eventManager.setInitialSimulationTime(initialTime);
        idList = model.getListOfInstantaneousTransitionsID();
        for (Integer id : idList)
            if (model.evaluateGuard(id))
                eventManager.addEnabledEvent(id, model.computeRate(id));
    }
  
    /**
     * sets the initial time of the simulation
     * @param time
     */
    public void setInitialTime(double time) {
        if (this.initialized)
            throw new SimulationException("Cannot change initial time: simualtor already initialized");
        this.initialTime = time;
    }
    /**
     * Sets the final time of the simulation
     * @param time
     */
    public void setFinalTime(double time) {
        if (this.initialized)
            throw new SimulationException("Cannot change final time: simualtor already initialized");
        this.finalTime = time;
    }
   
    
   /**
    * The main function. Runs the simulation
    */
    public void run() {
        next = new NextEvent();
        long simTime = System.nanoTime();
        this.lastSimulationStochasticSteps = 0;
        this.lastSimulationInstantaneousSteps = 0;
        this.lastSimulationTimedSteps = 0;
        this.lastSimulationDelayedSteps = 0;
        
        //Debugger.put("Simulation started");
        currentTime = this.initialTime;
        collector.putInitialState(model.getVariablesValues(), currentTime);

        int percentage = 1;

        while (true) {
            //Debugger.put("step " + this.lastSimulationSteps);
            //Debugger.put("current time: " + currentTime);
            //Debugger.put("Model state: " + model.getVariablesValues().toString());
            
            if(currentTime > finalTime*percentage/100)
                Debugger.put("Completed " + (percentage++) + "% in time " + ((double)System.nanoTime() - simTime)/1000000);


            //identify next transition
            nextTransition();
            //Debugger.put("Executing transition " + model.getTransitionName(next.nextTransition) + " with id " + next.nextTransition);

            //print + check termination
            //termination by end time reached
            if (next.nextTime > this.finalTime) {

                //Debugger.put("Final time reached");

                double[] v = model.getVariablesValues();
                if (collector.dataNeeded(this.finalTime))
                    collector.putData(v, this.finalTime);
                collector.putFinalState(v, finalTime);

                //Debugger.put("Simulation is over");

                break;
            }
            if (collector.dataNeeded(next.nextTime)) {

                //Debugger.put("Sending data to collector");

                double[]  v = model.getVariablesValues();
                collector.putData(v, next.nextTime);
            }
            //update time
            currentTime = next.nextTime;
            //update model
            executeTransition();

            //Debugger.put("Transition executed and store updated");

            //termination by forced stop
            if (next.stop) {

                //Debugger.put("Termination forced by transition");

                double[] v = model.getVariablesValues();
                collector.putFinalState(v, currentTime);
                break;
            }
        }
        this.lastSimulationTime = (double)(System.nanoTime() - simTime)/1000000;
    }


    /**
     * chooses the next transition to be executed
     * @param next the data structure where the next transition info will be saved
     * @param currentTime
     */
    private void nextTransition() {
        double nextTime, nextEventTime;
        if (this.eventManager.areInstantaneousEventsEnabled()) {
            this.lastSimulationInstantaneousSteps++;
            //fire an instantaneous transition
            next.nextTime = currentTime;
            next.nextTransition = this.eventManager.getNextEvent(rand,currentTime);
            if (model.isStopping(next.nextTransition))
                next.stop = true;
            //next.isDelayed = false;
            return;
        }
        nextTime = this.currentTime + RandomGenerator.expDist(this.rateManager.getExitRate(), rand);
        nextEventTime = this.eventQueue.getNextFiringTime();
        if (nextEventTime <= nextTime) {
            next.nextTime = nextEventTime;
            next.nextTransition = this.eventQueue.extractFiringEvent();
            if (this.eventQueue.isLastEventDelayed()) {
                this.lastSimulationDelayedSteps++;
                next.isDelayed = true;
                if (model.isStoppingAfterDelay(next.nextTransition))
                    next.stop = true;
            } else
                this.lastSimulationTimedSteps++;
            if(model.isStopping(next.nextTransition))
                next.stop = true;
            return;
        }
        else {
            this.lastSimulationStochasticSteps++;
            next.nextTime = nextTime;
            next.nextTransition = this.rateManager.sampleNextTransition(rand);
            if (model.isStopping(next.nextTransition))
                next.stop = true;
            //next.isDelayed = false;
            return;
        }
    }



    private void executeTransition() {
        switch ( model.getTransitionType(next.nextTransition) ) {
            case STOCHASTIC :
                this.model.execute(next.nextTransition);
                updateModel(next.nextTransition);
                break;
            case INSTANTANEOUS :
                //an event should become inactive due to its update
                //this.eventManager.removeEvent(next.nextTransition);
                this.model.execute(next.nextTransition);
                updateModel(next.nextTransition);
                break;
            case TIMED :
                if (next.isDelayed) {
                    throw new SimulationException("Delayed events still have to be implemented");
                    /*
                     * devo aggiungere ai modelli il grafo delle dipendenze dei delaied updates.
                     * e cambiare updatemodel in modo che scelga se prendere i grafi delle dipendenze degli update
                     * normali o delayed.
                     */
                    //event.isDelayed = false;
                } else {
                    this.model.execute(next.nextTransition);
                    double newFiringTime = model.computeNextFiringTime(next.nextTransition, currentTime);
                    if (newFiringTime > this.currentTime)
                        eventQueue.addEvent(next.nextTransition, newFiringTime);
                    else
                        eventQueue.addEvent(next.nextTransition, Double.POSITIVE_INFINITY);
                    updateModel(next.nextTransition);
                }
                break;
            default:
                throw new SimulationException("Incompatible transition type: " + model.getTransitionType(next.nextTransition) + ", aborting!");
        }

    }



    /**
     * updates guards, rates and firing times
     * @param id the id of the fired transition
     */
    private void updateModel(int id) {
        //HashSet<Integer> guard = this.model.getListOfUpdatedGuards(id);
        //HashSet<Integer> rates = this.model.getListOfUpdatedRates(id);
        //HashSet<Integer> firing = this.model.getListOfUpdatedFiringTimes(id);

        for (Integer j : this.model.getListOfUpdatedGuards(id)) {
            boolean status = this.model.getCurrentGuardStatus(j);
            if (status != this.model.evaluateGuard(j)) {
                switch (this.model.getTransitionType(j)) {
                    case STOCHASTIC :
                        //transition has been activated if status was false
                        //schedule computation of rate.
                        //HERE COMPUTATION IS DONE IMMEDAITELY. MAY BE PERFORMED TWICE
                        //BUT THE MECHANISM TO SCHEDULE THEM WAS NOT SO EFFICIENT?
                        if (!status)
                            //rates.add(j);
                            rateManager.updateRate(j, model.computeRate(j));
                        else
                            this.rateManager.updateRate(j, 0.0);
                    break;
                    case INSTANTANEOUS :
                        if (!status) {
                            //this.eventManager.addEnabledEvent(j,1);
                            //rates.add(j);
                            this.eventManager.addEnabledEvent(j, model.computeRate(j));
                        }
                        else
                            this.eventManager.removeEvent(j);
                        break;
                    case TIMED :
                        if (!status)
                           //firing.add(j);
                           eventQueue.updateEvent(j, model.computeNextFiringTime(j, currentTime));
                        else
                           eventQueue.updateEvent(j, Double.POSITIVE_INFINITY);
                        break;
                    default:
                        throw new SimulationException("Incompatible transition type");
                }
            }
        }
        for (Integer j : this.model.getListOfUpdatedRates(id)) {
            switch (this.model.getTransitionType(j)) {
                case STOCHASTIC :
                    rateManager.updateRate(j, model.computeRate(j));
                    break;
                case INSTANTANEOUS :
                    eventManager.updatePriority(j, model.computeRate(j));
                    break;
                case TIMED :
                    throw new SimulationException("Trying to compute rate for a timed transitions, that has no rate.");
                default:
                    throw new SimulationException("Incompatible transition type");
            }
        }
        for (Integer j : this.model.getListOfUpdatedFiringTimes(id))
            eventQueue.updateEvent(j, model.computeNextFiringTime(j, currentTime));
    } 

    /**
     * Resets the simulator, so that it can be reinitialized with another model
     * or other simulation parameters
     */
    public void reset(DataCollector dc) {
        this.collector = dc;
        this.initialized = false;
        this.eventQueue = null;
        this.eventManager = null;
        this.rateManager = null;
    }

    public void setProgressMonitor(ProgressMonitor monitor) {
        throw new RuntimeException("To be implemented");
    }

    public double getLastSimulationTime() {
        return this.lastSimulationTime;
    }

    public long getLastSimulationSteps() {
        return this.lastSimulationDelayedSteps + this.lastSimulationInstantaneousSteps +
                this.lastSimulationStochasticSteps + this.lastSimulationTimedSteps;
    }

    public long getLastSimulationDelayedSteps() {
        return lastSimulationDelayedSteps;
    }

    public long getLastSimulationInstantaneousSteps() {
        return lastSimulationInstantaneousSteps;
    }

    public long getLastSimulationStochasticSteps() {
        return lastSimulationStochasticSteps;
    }

    public long getLastSimulationTimedSteps() {
        return lastSimulationTimedSteps;
    }

    

    private class NextEvent {
        public int nextTransition;
        public double nextTime;
        public boolean stop;
        public boolean isDelayed;

        public NextEvent() {
            this.isDelayed = false;
            this.stop = false;
        }
    }

}
